Multitasking


This page describes Pog's ability to perform multiple tasks simultaneously. It explains how to write tasks, how to start new tasks running, and how to manipulate tasks which are running.

Overview

A task is an isolated context for executing Pog code. Tasks are unlike typical operating-system threads1 in that they do not share the same address space2. All tasks are protected from each other so that a task cannot manipulate the variables in another task. This is not the case with threads. Tasks do in a sense share a global address space - they are all running in the same game environment, and have shared access to the game objects and data.

All Pog code executing in the game is executing as a task, and the script engine allocates execution time to each task in turn. Flux provides the facility to start script functions as tasks (using FcScriptEngine::StartTask) or as function calls (using FcScriptEngine::CallFunction). Almost all gameplay code executes as tasks, since it executes while the game is running - calling a function while the game is running forces the entire game to wait for the function to finish, which is not usually the desired behaviour.

Task functions

A task is executed by starting a task function. This is a normal Pog function declared with the task keyword. Task functions may not have return values, since the execution of the task may take some time and the return value would have to be immediately available for the calling code to store. For example:
// This is a normal function prototype
prototype int normal_function( int parameter );

// This is a task function prototype
prototype task task_function( int parameter ); 

Note:
The task keyword in front of the task function is not a return value, it is a special declaration syntax to mark this function as a task function.

There are no other restrictions on task functions. They are in all other respects identical to normal functions.

Important:
A task function is not the same thing as a task. The task is the flow of program execution, and the task function is the program which directs the flow. Several tasks may start the same task function, but they are all different tasks. As an analogy, the task function is like a road, and the task like a car - the road guides the car, and the car represents progress along the road. Many cars can drive on the same road, and a car can leave one road and drive on another. This is like having a task function call other functions.

Starting a task

Normal functions are executed by calling them. This uses the normal C function call syntax whereby a function name followed by parameters in parentheses is a call expression whose type is the return type of the function.

Tasks are executed by starting a task function. This uses the same syntax, but the name of the function is preceded by the keyword start. This forms a start expression whose type is htask. The value of the function start expression is the handle to the task which has been started. The code which starts the task may store this handle in a variable and use it to manipulate the task via the API in the Task package.

Calling task functions or starting normal functions is not possible. The execution type must match the function type. This prevents code which is intended to be executed atomically3 from being executed as a task, and vice-versa.

For example, a task function may include an infinite loop which waits for events to be signalled by the game. If the function were called atomically this loop would never receive events (time would not pass in the game while the loop executed) and the loop would not terminate. This would cause the task which had called the function to lock up.

The following example shows a task being started:
package TaskTest;

uses Debug;

provides Main;

// This task function prints the numbers from 1 to n
// to the log.
task log_n_numbers( int n )
{
	int i;

	for ( i = 1; i < n; ++i )
	    Debug.PrintInt( i );
}

// This function starts three different print tasks to
// print different lists of numbers.
Main()
{
	start log_n_numbers( 10 );
	start log_n_numbers( 20 );
	start log_n_numbers( 30 );
} 

Running the Main function in the example will start three tasks using the task function log_n_numbers. If you compile and run this package, the debug log file will contain the numbers 1 to 10, 1 to 20, and 1 to 30. The number sequences will probably be interleaved with other because the tasks are all running at the same time and do not attempt to synchronise their output.

Task handles

In the Flux engine, Pog tasks are represented as instances of the C++ class FcScriptTask. As described in the handle types section, this means that Pog code may use handles to access and manipulate tasks. Pog provides the following built-in type to represent tasks:

handle htask : hobject;

When a task is started from within Pog code, the handle to the task is provided automatically and appears to be a return value (although it is not really a return value - task functions have no return value.)

If you wish to access and manipulate the task after it has been created, you can store the task handle in a variable using an assignment in the usual way.

The current task

The Task package provides a function - Task.Current - for returning the handle of the current task. This function can always be used to obtain a handle for, and therefore control, the task currently executing a given piece of code.

Halting a task

A task which has finished execution is said to be halted. When a task is halted, the engine destroys the C++ object which represents the task. This means a halted task may never restart.

Tasks halt automatically when the flow of execution returns from the task function which was started to create the task. Tasks may be halted explicitly, if their handle is known, using the function Task.Halt. The current task may be halted in this manner if desired.

When a task halts, its child tasks (tasks which were started by that task) are also halted. If you need a child task to outlive its parent, then that task must be detached and made a new top level task, using the function Task.Detach.


1 Threads are strands of program execution. Multiple threads may execute simultaneously within a program, with each thread having access to all the code and data in the program.

2 Address spaces are the environments in which code is executed. The address space contains all the data which the executing code may access, or address.

3 Atomic execution is when a function is called as single operation. The function executes as soon as it is called, and control does not return to the point of call until the function has finished executing. This is the normal type of function execution.